// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'package:_fe_analyzer_shared/src/testing/features.dart';
import 'package:expect/async_helper.dart';
import 'package:compiler/src/closure.dart';
import 'package:compiler/src/common.dart';
import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/elements/names.dart';
import 'package:compiler/src/elements/types.dart';
import 'package:compiler/src/js_backend/runtime_types_resolution.dart';
import 'package:compiler/src/js_model/js_world.dart';
import 'package:compiler/src/js_model/element_map.dart';
import 'package:compiler/src/kernel/element_map.dart';
import 'package:compiler/src/kernel/kernel_strategy.dart';
import 'package:compiler/src/universe/feature.dart';
import 'package:compiler/src/universe/resolution_world_builder.dart';
import 'package:compiler/src/universe/selector.dart';
import 'package:kernel/ast.dart' as ir;
import '../equivalence/check_helpers.dart';
import '../equivalence/id_equivalence.dart';
import '../equivalence/id_equivalence_helper.dart';

main(List<String> args) {
  runTests(args);
}

runTests(List<String> args, [int? shardIndex]) {
  asyncTest(() async {
    Directory dataDir = Directory.fromUri(Platform.script.resolve('data'));
    await checkTests(
      dataDir,
      const RtiNeedDataComputer(),
      args: args,
      shardIndex: shardIndex ?? 0,
      shards: shardIndex != null ? 4 : 1,
    );
  });
}

class Tags {
  static const String needsTypeArguments = 'needsArgs';
  static const String needsSignature = 'needsSignature';
  static const String dependencies = 'deps';
  static const String explicitTypeCheck = 'explicit';
  static const String implicitTypeCheck = 'implicit';
  static const String typeArgumentTest = 'test';
  static const String typeLiteral = 'exp';
  static const String selectors = 'selectors';
  static const String instantiationsNeedTypeArguments = 'needsInst';
}

mixin ComputeValueMixin {
  Compiler get compiler;

  KernelFrontendStrategy get frontendStrategy => compiler.frontendStrategy;
  ResolutionWorldBuilder get resolutionWorldBuilder =>
      compiler.resolutionWorldBuilderForTesting!;
  RuntimeTypesNeedBuilderImpl get rtiNeedBuilder =>
      frontendStrategy.runtimeTypesNeedBuilderForTesting
          as RuntimeTypesNeedBuilderImpl;
  RuntimeTypesNeedImpl get rtiNeed =>
      compiler.backendClosedWorldForTesting!.rtiNeed as RuntimeTypesNeedImpl;
  ClassEntity? getFrontendClass(ClassEntity cls);
  MemberEntity? getFrontendMember(MemberEntity member);
  Local? getFrontendClosure(MemberEntity member);

  void findChecks(
    Features features,
    String key,
    Entity? entity,
    Set<DartType> checks,
  ) {
    Set<DartType> types = Set<DartType>();
    FindTypeVisitor finder = FindTypeVisitor(entity);
    for (DartType type in checks) {
      if (type.accept(finder, null)) {
        types.add(type);
      }
    }
    List<String> list = types.map(typeToString).toList()..sort();
    if (list.isNotEmpty) {
      features[key] = '[${list.join(',')}]';
    }
  }

  void findDependencies(Features features, Entity? entity) {
    Iterable<Entity> dependencies = entity == null
        ? const []
        : rtiNeedBuilder.typeVariableTestsForTesting!
              .getTypeArgumentDependencies(entity);
    if (dependencies.isNotEmpty) {
      List<String> names = dependencies.map((Entity d) {
        if (d is MemberEntity && d.enclosingClass != null) {
          return '${d.enclosingClass!.name}.${d.name}';
        }
        return d.name!;
      }).toList()..sort();
      features[Tags.dependencies] = '[${names.join(',')}]';
    }
  }

  String getClassValue(ClassEntity backendClass) {
    Features features = Features();

    if (rtiNeed.classNeedsTypeArguments(backendClass)) {
      features.add(Tags.needsTypeArguments);
    }
    ClassEntity? frontendClass = getFrontendClass(backendClass);
    findDependencies(features, frontendClass);
    if (rtiNeedBuilder.classesUsingTypeVariableLiterals.contains(
      frontendClass,
    )) {
      features.add(Tags.typeLiteral);
    }
    if (rtiNeedBuilder.typeVariableTestsForTesting!.classTestsForTesting
        .contains(frontendClass)) {
      features.add(Tags.typeArgumentTest);
    }
    findChecks(
      features,
      Tags.explicitTypeCheck,
      frontendClass,
      rtiNeedBuilder.typeVariableTestsForTesting!.explicitIsChecks,
    );
    findChecks(
      features,
      Tags.implicitTypeCheck,
      frontendClass,
      rtiNeedBuilder.typeVariableTestsForTesting!.implicitIsChecks,
    );
    return features.getText();
  }

  String getMemberValue(MemberEntity backendMember) {
    MemberEntity? frontendMember = getFrontendMember(backendMember);
    Local? frontendClosure = getFrontendClosure(backendMember);

    Features features = Features();

    if (backendMember is FunctionEntity) {
      if (rtiNeed.methodNeedsTypeArguments(backendMember)) {
        features.add(Tags.needsTypeArguments);
      }
      if (rtiNeed.methodNeedsSignature(backendMember)) {
        features.add(Tags.needsSignature);
      }

      void addFrontendData(Entity entity) {
        findDependencies(features, entity);
        if (rtiNeedBuilder.typeVariableTestsForTesting!.methodTestsForTesting
            .contains(entity)) {
          features.add(Tags.typeArgumentTest);
        }
        findChecks(
          features,
          Tags.explicitTypeCheck,
          entity,
          rtiNeedBuilder.typeVariableTestsForTesting!.explicitIsChecks,
        );
        findChecks(
          features,
          Tags.implicitTypeCheck,
          entity,
          rtiNeedBuilder.typeVariableTestsForTesting!.implicitIsChecks,
        );
        rtiNeedBuilder.selectorsNeedingTypeArgumentsForTesting?.forEach((
          Selector selector,
          Set<Entity> targets,
        ) {
          if (targets.contains(entity)) {
            features.addElement(Tags.selectors, selector);
          }
        });
        rtiNeedBuilder
            .instantiatedEntitiesNeedingTypeArgumentsForTesting[entity]
            ?.forEach((GenericInstantiation instantiation) {
              features.addElement(
                Tags.instantiationsNeedTypeArguments,
                instantiation.shortText,
              );
            });
      }

      if (frontendClosure != null) {
        addFrontendData(frontendClosure);
        if (rtiNeedBuilder.localFunctionsUsingTypeVariableLiterals.contains(
          frontendClosure,
        )) {
          features.add(Tags.typeLiteral);
        }
      } else if (frontendMember != null) {
        addFrontendData(frontendMember);
        if (rtiNeedBuilder.methodsUsingTypeVariableLiterals.contains(
          frontendMember,
        )) {
          features.add(Tags.typeLiteral);
        }
      }
    }
    return features.getText();
  }
}

/// Visitor that determines whether a type refers to [entity].
class FindTypeVisitor extends DartTypeVisitor<bool, Null> {
  final Entity? entity;

  FindTypeVisitor(this.entity);

  bool check(DartType type) => visit(type, null);

  bool checkList(List<DartType> types) {
    for (DartType type in types) {
      if (check(type)) {
        return true;
      }
    }
    return false;
  }

  @override
  bool visitNullableType(NullableType type, _) => visit(type.baseType, null);

  @override
  bool visitNeverType(NeverType type, _) => false;

  @override
  bool visitVoidType(VoidType type, _) => false;

  @override
  bool visitTypeVariableType(TypeVariableType type, _) =>
      type.element.typeDeclaration == entity;

  @override
  bool visitFunctionTypeVariable(FunctionTypeVariable type, _) => false;

  @override
  bool visitFunctionType(FunctionType type, _) =>
      type.returnType.accept(this, null) ||
      checkList(type.typeVariables) ||
      checkList(type.parameterTypes) ||
      checkList(type.optionalParameterTypes) ||
      checkList(type.namedParameterTypes);

  @override
  bool visitInterfaceType(InterfaceType type, _) =>
      type.element == entity || checkList(type.typeArguments);

  @override
  bool visitRecordType(RecordType type, _) => throw UnimplementedError();

  @override
  bool visitDynamicType(DynamicType type, _) => false;

  @override
  bool visitErasedType(ErasedType type, _) => false;

  @override
  bool visitAnyType(AnyType type, _) => false;

  @override
  bool visitFutureOrType(FutureOrType type, _) => check(type.typeArgument);
}

class RtiNeedDataComputer extends DataComputer<String> {
  const RtiNeedDataComputer();

  /// Compute RTI need data for [member] from the new frontend.
  ///
  /// Fills [actualMap] with the data.
  @override
  void computeMemberData(
    Compiler compiler,
    MemberEntity member,
    Map<Id, ActualData<String>> actualMap, {
    bool verbose = false,
  }) {
    JClosedWorld closedWorld = compiler.backendClosedWorldForTesting!;
    JsToElementMap elementMap = closedWorld.elementMap;
    MemberDefinition definition = elementMap.getMemberDefinition(member);
    RtiNeedIrComputer(
      compiler.reporter,
      actualMap,
      elementMap,
      compiler,
      closedWorld.closureDataLookup,
    ).run(definition.node);
  }

  /// Compute RTI need data for [cls] from the new frontend.
  ///
  /// Fills [actualMap] with the data.
  @override
  void computeClassData(
    Compiler compiler,
    ClassEntity cls,
    Map<Id, ActualData<String>> actualMap, {
    bool verbose = false,
  }) {
    JClosedWorld closedWorld = compiler.backendClosedWorldForTesting!;
    JsToElementMap elementMap = closedWorld.elementMap;
    RtiNeedIrComputer(
      compiler.reporter,
      actualMap,
      elementMap,
      compiler,
      closedWorld.closureDataLookup,
    ).computeForClass(elementMap.getClassDefinition(cls).node as ir.Class);
  }

  @override
  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
}

mixin IrMixin implements ComputeValueMixin {
  @override
  MemberEntity? getFrontendMember(MemberEntity backendMember) {
    ElementEnvironment elementEnvironment =
        compiler.frontendClosedWorldForTesting!.elementEnvironment;
    LibraryEntity frontendLibrary = elementEnvironment.lookupLibrary(
      backendMember.library.canonicalUri,
    )!;
    if (backendMember.enclosingClass != null) {
      if (backendMember.enclosingClass!.isClosure) return null;
      ClassEntity frontendClass = elementEnvironment.lookupClass(
        frontendLibrary,
        backendMember.enclosingClass!.name,
      )!;
      if (backendMember is ConstructorEntity) {
        return elementEnvironment.lookupConstructor(
          frontendClass,
          backendMember.name!,
        );
      } else {
        return elementEnvironment.lookupClassMember(
          frontendClass,
          Name(
            backendMember.name!,
            frontendClass.library.canonicalUri,
            isSetter: backendMember.isSetter,
          ),
        );
      }
    }
    return elementEnvironment.lookupLibraryMember(
      frontendLibrary,
      backendMember.name!,
      setter: backendMember.isSetter,
    );
  }

  @override
  ClassEntity? getFrontendClass(ClassEntity backendClass) {
    if (backendClass.isClosure) return null;
    ElementEnvironment elementEnvironment =
        compiler.frontendClosedWorldForTesting!.elementEnvironment;
    LibraryEntity frontendLibrary = elementEnvironment.lookupLibrary(
      backendClass.library.canonicalUri,
    )!;
    return elementEnvironment.lookupClass(frontendLibrary, backendClass.name);
  }

  @override
  Local? getFrontendClosure(MemberEntity member) {
    JClosedWorld closedWorld = compiler.backendClosedWorldForTesting!;
    ir.Node node = closedWorld.elementMap.getMemberDefinition(member).node;
    if (node is ir.FunctionDeclaration || node is ir.FunctionExpression) {
      KernelFrontendStrategy frontendStrategy = compiler.frontendStrategy;
      KernelToElementMap frontendElementMap = frontendStrategy.elementMap;
      return frontendElementMap.getLocalFunction(node as ir.LocalFunction);
    }
    return null;
  }
}

class RtiClassNeedIrComputer
    with
        DataRegistry<String>,
        ComputeValueMixin,
        IrMixin,
        IrDataRegistryMixin<String> {
  @override
  final Compiler compiler;
  final JsToElementMap _elementMap;
  @override
  final Map<Id, ActualData<String>> actualMap;

  RtiClassNeedIrComputer(this.compiler, this._elementMap, this.actualMap);

  @override
  DiagnosticReporter get reporter => compiler.reporter;

  void computeClassValue(ClassEntity cls) {
    Id id = ClassId(cls.name);
    final node = _elementMap.getClassDefinition(cls).node as ir.TreeNode;
    ir.TreeNode nodeWithOffset = computeTreeNodeWithOffset(node)!;
    registerValue(
      nodeWithOffset.location!.file,
      nodeWithOffset.fileOffset,
      id,
      getClassValue(cls),
      cls,
    );
  }
}

/// AST visitor for computing inference data for a member.
class RtiNeedIrComputer extends IrDataExtractor<String>
    with ComputeValueMixin, IrMixin {
  final JsToElementMap _elementMap;
  final ClosureData _closureDataLookup;
  @override
  final Compiler compiler;

  RtiNeedIrComputer(
    DiagnosticReporter reporter,
    Map<Id, ActualData<String>> actualMap,
    this._elementMap,
    this.compiler,
    this._closureDataLookup,
  ) : super(reporter, actualMap);

  @override
  String computeClassValue(Id id, ir.Class node) {
    return getClassValue(_elementMap.getClass(node));
  }

  @override
  String computeMemberValue(Id id, ir.Member node) {
    return getMemberValue(_elementMap.getMember(node));
  }

  @override
  String? computeNodeValue(Id id, ir.TreeNode node) {
    if (node is ir.FunctionExpression || node is ir.FunctionDeclaration) {
      ClosureRepresentationInfo info = _closureDataLookup.getClosureInfo(
        node as ir.LocalFunction,
      );
      return getMemberValue(info.callMethod!);
    }
    return null;
  }
}
